package com.onpositive.datamap.core.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

import com.onpositive.datamap.core.IDataProperty;
import com.onpositive.datamap.core.IScalarProperty;
import com.onpositive.datamap.values.impl.ScalarRange;
import com.onpositive.semantic.model.api.property.FunctionOperator;
import com.onpositive.semantic.model.api.property.IFunction;
import com.onpositive.semantic.model.api.property.ValueUtils;
import com.onpositive.semantic.model.groups.BasicGroupingOperators;
import com.onpositive.semantic.model.groups.BasicGroupingOperators.Group;

public class ScalarRangeGroupingCalculator extends FunctionOperator {

	protected static final double E = 0.0000001;
	private static final int MAX_UNRANGED_COUNT = 8;

	protected int rangeCount;

	public ScalarRangeGroupingCalculator(int rangeCount) {
		super();
		this.rangeCount = rangeCount;
	}

	@SuppressWarnings("unchecked")
	public List<Group> calculateGroups(IFunction q, Object vl) {
		Collection<Object> c=ValueUtils.toCollection(vl);
	
		HashMap<Object, ArrayList<Object>> groups = new HashMap<Object, ArrayList<Object>>();
		Object[] allValues = getValuesForField(c,q);

		// remove nulls;
		ArrayList<Object> notNulls = new ArrayList<Object>(allValues.length);
		
		ArrayList<Object> unknowns = new ArrayList<Object>(allValues.length);
		ArrayList<Object> nones = new ArrayList<Object>(allValues.length);
		ArrayList<Object> nulls = new ArrayList<Object>();
		ScalarRange nv=null;
		ScalarRange uv=null;
		int a=0;
		for (Object qq:c) {
			if (allValues[a] == null) {
				nulls.add(qq);
			} else {
				ScalarRange m=(ScalarRange) allValues[a];
				if (m.isNone()){
					nones.add(qq);
					nv=m;
					a++;
					continue;
				}
				if (m.isVarious()){
					unknowns.add(qq);
					uv=m;
					a++;
					continue;
				}
				notNulls.add(allValues[a]);
			}
			a++;
		}
		allValues = notNulls.toArray();
		if (allValues.length == 0) {
			ArrayList<Group> arrayList = new ArrayList<Group>();
			if (nulls.size() > 0) {
				Group z = new Group(null,q, nulls.toArray());
				arrayList.add(z);
			}
			return arrayList;
		}
		Arrays.sort(allValues);
		HashSet<Object>m=new HashSet<Object>(Arrays.asList(allValues));
		if (m.size()<MAX_UNRANGED_COUNT){
			return (List<Group>) BasicGroupingOperators.getInstance().calc(q, c);
		}
		int rangeLength = allValues.length / this.rangeCount + 1;
		List<ScalarRange> ranges = new ArrayList<ScalarRange>(this.rangeCount);
		IDataProperty property = null;
		if (q instanceof IDataProperty)
			property = (IDataProperty) q;
		if (rangeLength >= allValues.length) // Then we have only one interval
		{
			ScalarRange range = createSuitableRange(property,null,((Number) allValues[0]).doubleValue(),
					((Number) allValues[allValues.length - 1]).doubleValue());
			groups.put(range, new ArrayList<Object>());
			ranges.add(range);
		} else {
			Number prev = getNumber(allValues[0]);
			for (int i = 1; i < allValues.length; i++) {
				if ((i % rangeLength == 0) || (i == allValues.length - 1)) {
					ScalarRange range = createSuitableRange(property,null,prev.doubleValue(),
							getNumber(allValues[i]).doubleValue());
					ranges.add(range);
					groups.put(range, new ArrayList<Object>());
					if (i < allValues.length - 1) {
						prev = getNumber(allValues[i + 1]);
					} else {
						prev = getNumber(allValues[i]);
					}
				}
			}
		}

		for (Object item: c) {
			Object propertyValue = q.getValue(item);
			if (propertyValue == null) {
				continue;
			}
			ScalarRange mq=(ScalarRange) propertyValue;
			if (mq.isNone()){
				continue;
			}
			if (mq.isVarious()){
				continue;
			}
			ScalarRange range = this.getRange(propertyValue, ranges);
			ArrayList<Object> arrayList = groups.get(range);
			arrayList.add(item);
		}
		List<Group> result = new ArrayList<Group>(groups.size());
		for (Object key : groups.keySet()) {
			Object[] valuesArray = groups.get(key).toArray();
			if (valuesArray.length > 0) {
				this.doRangeCorrection((ScalarRange) key, valuesArray, q);
				Group z = new Group( key,q, valuesArray);
				result.add(z);
			}
		}
		Collections.sort(result,new Comparator<Group>(){

			@Override
			public int compare(Group group1, Group group2) {
				return (int) Math.signum(((ScalarRange)group1.getElement()).getHighValue() - ((ScalarRange)group2.getElement()).getHighValue());
			}
			
		});
		if (unknowns.size() > 0) {
			Group z = new Group(uv,q, unknowns.toArray());
			result.add(z);
		}
		if (nones.size() > 0) {
			Group z = new Group(nv,q, nones.toArray());
			result.add(z);
		}
		if (nulls.size() > 0) {
			Group z = new Group(null,q, nulls.toArray());
			result.add(z);
		}
		return result;
	}
	
	ScalarRange createSuitableRange(IDataProperty property, Object comment, double low, double high) {
		IScalarProperty p=(IScalarProperty) property;
		if (p ==null){
			return new ScalarRange(property, "", low,high); 
		}
		return p.newValue(low,high,comment);		
	}
	
	protected Number getNumber(Object object) {
		if (object instanceof Number)
			return (Number) object;
		if (object instanceof ScalarRange)
			return ((ScalarRange) object).getHigh();
		return null;
	}

	protected ScalarRange getRange(Object propertyValue,
			List<ScalarRange> ranges) {
		for (ScalarRange range : ranges) {
			if (compare(getNumber(propertyValue),range) == 0) {
				return range;
			}
		}
		throw new AssertionError("Can't find range for " + propertyValue);
	}
	
	public int compare(Number value, ScalarRange range) {
		if (value.doubleValue() < range.getLowValue() - E) {
			return 1;
		}
		if (value.doubleValue() > range.getHighValue() + E) {
			return -1;
		}
		return 0;
	}

	private Object[] getValuesForField(Collection<Object> c, IFunction q) {
		Object[] vl=new Object[c.size()];
		int a=0;
		for (Object qm:c){
			vl[a++]=q.getValue(qm);
		}
		return vl;
	}

	protected void doRangeCorrection(ScalarRange key, Object[] records,
			IFunction field) {
		if (records.length == 0) {
			return;
		}
		double min = ((Number) getNumber(field.getValue(records[0])))
				.doubleValue();
		double max = min;
		int minIdx = 0;
		int maxIdx = 0;
		for (int i = 1; i < records.length; i++) {
			double value = ((Number) getNumber(field.getValue(records[i]))).doubleValue();
			if (value < min) {
				min = value;
				minIdx = i;
			}
			if (value > max) {
				max = value;
				maxIdx = i;
			}
		}
		Number start = (Number) getNumber(field.getValue(records[minIdx]));
		Number end = (Number) getNumber(field.getValue(records[maxIdx]));
		key = createSuitableRange(key.getProperty(),key.getComment(),start.doubleValue(),end.doubleValue());
	}
	
	@Override
	protected Object calc(IFunction f, Object o2) {
		return calculateGroups(f, o2);
	}

}
